const config = require('./config');
const gulp = require('gulp');
const gutil = require('gulp-util');
const frep = require('gulp-frep');
const fs = require('fs');
const args = require('minimist')(process.argv.slice(2));
const path = require('path');
const rename = require('gulp-rename');
const filter = require('gulp-filter');
const concat = require('gulp-concat');
const series = require('stream-series');
const lazypipe = require('lazypipe');
const glob = require('glob').sync;
const uglify = require('gulp-uglify');
const sass = require('gulp-sass')(require('sass'));
const plumber = require('gulp-plumber');
const ngAnnotate = require('gulp-ng-annotate');
const insert = require('gulp-insert');
const gulpif = require('gulp-if');
const cssnano = require('cssnano');
const gulpPostcss = require('gulp-postcss');
const postcss = require('postcss');
const _ = require('lodash');
const constants = require('./const');
const VERSION = constants.VERSION;
const BUILD_MODE = constants.BUILD_MODE;
const IS_DEV = constants.IS_DEV;
const ROOT = constants.ROOT;
const utils = require('../scripts/gulp-utils.js');

exports.buildJs = buildJs;
exports.autoprefix = utils.autoprefix;
exports.buildModule = buildModule;
exports.filterNonCodeFiles = filterNonCodeFiles;
exports.readModuleArg = readModuleArg;
exports.themeBuildStream = themeBuildStream;
exports.minifyCss = minifyCss;
exports.dedupeCss = dedupeCss;
exports.args = args;

/**
 * Builds the entire component library javascript.
 */
function buildJs() {
  const jsFiles = config.jsCoreFiles;
  config.componentPaths.forEach(component => {
    jsFiles.push(path.join(component, '*.js'));
    jsFiles.push(path.join(component, '**/*.js'));
  });

  gutil.log("building js files...");

  const jsBuildStream = gulp.src(jsFiles)
  .pipe(filterNonCodeFiles())
  .pipe(utils.buildNgMaterialDefinition())
  .pipe(plumber())
  .pipe(ngAnnotate())
  .pipe(utils.addJsWrapper(true));

  const jsProcess = series(jsBuildStream, themeBuildStream())
  .pipe(concat('angular-material.js'))
  .pipe(BUILD_MODE.transform())
  .pipe(insert.prepend(config.banner))
  .pipe(insert.append(';window.ngMaterial={version:{full: "' + VERSION + '"}};'))
  .pipe(gulp.dest(config.outputDir))
  .pipe(gulpif(!IS_DEV, uglify({output: {comments: 'some'}})))
  .pipe(rename({extname: '.min.js'}))
  .pipe(gulp.dest(config.outputDir));

  return series(jsProcess, deployMaterialMocks());

  // Deploy the `angular-material-mocks.js` file to the `dist` directory
  function deployMaterialMocks() {
    return gulp.src(config.mockFiles)
    .pipe(gulp.dest(config.outputDir));
  }
}

function minifyCss(extraOptions) {
  const options = {
    reduceTransforms: false,
    svgo: false
  };
  const preset = {
    preset: [
      'default',
      _.assign(options, extraOptions)
    ]
  };

  return gulpPostcss([cssnano(preset)]);
}

/**
 * @param {string} module
 * @param {{isRelease, minify, useBower}=} opts
 */
function buildModule(module, opts) {
  opts = opts || {};
  if (module.indexOf(".") < 0) {
    module = "material.components." + module;
  }
  gutil.log('Building ' + module + (opts.isRelease && ' minified' || '') + ' ...');

  const name = module.split('.').pop();
  utils.copyDemoAssets(name, 'src/components/', 'dist/demos/');

  let stream = utils.filesForModule(module)
  .pipe(filterNonCodeFiles())
  .pipe(filterLayoutAttrFiles())
  .pipe(gulpif('*.scss', buildModuleStyles(name)))
  .pipe(gulpif('*.js', buildModuleJs(name)));

  if (module === 'material.core') {
    stream = splitStream(stream);
  }

  return stream
  .pipe(BUILD_MODE.transform())
  .pipe(insert.prepend(config.banner))
  .pipe(gulpif(opts.minify, buildMin()))
  .pipe(gulpif(opts.useBower, buildBower()))
  .pipe(gulp.dest(BUILD_MODE.outputDir + name));

  function splitStream(stream) {
    const js = series(stream, themeBuildStream())
    .pipe(filter('**/*.js'))
    .pipe(concat('core.js'));

    const css = stream
    .pipe(filter(['**/*.css', '!**/ie_fixes.css']));

    return series(js, css);
  }

  function buildMin() {
    return lazypipe()
    .pipe(gulpif, /.css$/, minifyCss(),
      uglify({output: {comments: 'some'}})
      .on('error', function(e) {
          console.log('\x07', e.message);
          return this.end();
        }
      )
    )
    .pipe(rename, function(path) {
      path.extname = path.extname
      .replace(/.js$/, '.min.js')
      .replace(/.css$/, '.min.css');
    })
    ();
  }

  function buildBower() {
    return lazypipe()
    .pipe(utils.buildModuleBower, name, VERSION)();
  }

  function buildModuleJs(name) {
    const patterns = [
      {
        pattern: /@ngInject/g,
        replacement: 'ngInject'
      },
      {
        // Turns `thing.$inject` into `thing['$inject']` in order to prevent
        // Closure from stripping it from objects with an @constructor
        // annotation.
        pattern: /\.\$inject\b/g,
        replacement: "['$inject']"
      }
    ];
    return lazypipe()
    .pipe(plumber)
    .pipe(ngAnnotate)
    .pipe(frep, patterns)
    .pipe(concat, name + '.js')
    ();
  }

  /**
   * @param {string} name module name. I.e. 'select', 'dialog', etc.
   * @returns {*}
   */
  function buildModuleStyles(name) {
    let files = [];
    const inputVariableConsumers = [
      'input', 'select', 'checkbox', 'datepicker', 'radioButton', 'switch'
    ];
    config.themeBaseFiles.forEach(function(fileGlob) {
      files = files.concat(glob(fileGlob, {cwd: ROOT}));
    });

    // Handle md-input and md-input-container variables that need to be shared with md-select
    // in order to orchestrate identical layouts and alignments. In the future, it may be necessary
    // to also use these variables with md-datepicker and md-autocomplete.
    if (inputVariableConsumers.includes(name)) {
      files = files.concat(glob(config.inputVariables, {cwd: ROOT}));
    }

    const baseStyles = files.map(function(fileName) {
      return fs.readFileSync(fileName, 'utf8').toString();
    }).join('\n');

    let sassModules;
    // Don't add the Sass modules to core since they get automatically included already.
    if (name === 'core') {
      sassModules = '';
    } else {
      sassModules = fs.readFileSync(config.scssModules, 'utf8').toString();
    }

    return lazypipe()
    .pipe(insert.prepend, baseStyles)
    .pipe(gulpif, /theme.scss/, rename(name + '-default-theme.scss'), concat(name + '.scss'))
    // Theme files are suffixed with the `default-theme.scss` string.
    // In some cases there are multiple theme SCSS files, which should be concatenated together.
    .pipe(gulpif, /default-theme.scss/, concat(name + '-default-theme.scss'))
    // We can't prepend these earlier, or they get duplicated in a way that hoistScssAtUseStatements
    // can't deduplicate them.
    .pipe(insert.prepend, sassModules)
    .pipe(utils.hoistScssAtUseStatements)
    .pipe(sass)
    .pipe(dedupeCss)
    .pipe(utils.autoprefix)
    (); // Invoke the returning lazypipe function to create our new pipe.
  }
}

/**
 * @returns {string} module name. i.e. material.components.icon
 */
function readModuleArg() {
  const module = args.c ? 'material.components.' + args.c : (args.module || args.m);
  if (!module) {
    gutil.log('\nProvide a component argument via `-c`:',
      '\nExample: -c toast');
    gutil.log('\nOr provide a module argument via `--module` or `-m`.',
      '\nExample: --module=material.components.toast or -m material.components.dialog');
    throw new Error("Unable to read module arguments.");
  }
  return module;
}

/**
 * We are not injecting the layout-attributes selectors into the core module css,
 * otherwise we would have the layout-classes and layout-attributes in there.
 */
function filterLayoutAttrFiles() {
  return filter(function(file) {
    return !/.*layout-attributes\.scss/g.test(file.path);
  });
}

function filterNonCodeFiles() {
  return filter(function(file) {
    return !/demo|module\.json|script\.js|\.spec.js|README/.test(file.path);
  });
}

// builds the theming related css and provides it as a JS const for AngularJS
function themeBuildStream() {
  // Make a copy so that we don't modify the actual config that is used by other functions
  const paths = config.themeBaseFiles.slice(0);
  config.componentPaths.forEach(component => paths.push(path.join(component, '*-theme.scss')));
  paths.push(config.themeCore);

  return gulp.src(paths)
  .pipe(concat('default-theme.scss'))
  .pipe(utils.hoistScssVariables())
  .pipe(sass())
  .pipe(dedupeCss())
  // The PostCSS orderedValues plugin modifies the theme color expressions.
  .pipe(minifyCss({orderedValues: false}))
  .pipe(utils.cssToNgConstant('material.core', '$MD_THEME_CSS'));
}

// Removes duplicated CSS properties.
function dedupeCss() {
  const prefixRegex = /-(webkit|moz|ms|o)-.+/;

  return insert.transform(function(contents) {
    // Parse the CSS into an AST.
    const parsed = postcss.parse(contents);

    // Walk through all the rules, skipping comments, media queries etc.
    parsed.walk(function(rule) {
      // Skip over any comments, media queries and rules that have less than 2 properties.
      if (rule.type !== 'rule' || !rule.nodes || rule.nodes.length < 2) return;

      // Walk all of the properties within a rule.
      rule.walk(function(prop) {
        // Check if there's a similar property that comes after the current one.
        const hasDuplicate = validateProp(prop) && _.find(rule.nodes, function(otherProp) {
          return prop !== otherProp && prop.prop === otherProp.prop && validateProp(otherProp);
        });

        // Remove the declaration if it's duplicated.
        if (hasDuplicate) {
          prop.remove();

          gutil.log(gutil.colors.yellow(
            'Removed duplicate property: "' +
            prop.prop + ': ' + prop.value + '" from "' + rule.selector + '"...'
          ));
        }
      });
    });

    // Turn the AST back into CSS.
    return parsed.toResult().css;
  });

  // Checks if a property is a style declaration and that it
  // doesn't contain any vendor prefixes.
  function validateProp(prop) {
    return prop && prop.type === 'decl' && ![prop.prop, prop.value].some(function(value) {
      return value.indexOf('-') > -1 && prefixRegex.test(value);
    });
  }
}
